Healenium: Unlock stability in Appium Test Automation
Appium, a powerful tool for mobile test automation, often grapples with the persistent issue of test flakiness. This instability arises from the dynamic nature of mobile apps, where UI elements frequently change, leading to test failures and increased maintenance overhead. To address these challenges, Healenium emerges as a revolutionary solution, which can significantly enhance the reliability and help us to unlock the stability in Appium test automation.
Understanding the Appium Challenge
Appium, Although is very versatile, faces hurdles in maintaining test stability due to:
- Frequent UI changes: If Mobile app UI tends to change frequently sprint over sprint. Modifications in app layout or element attributes can render existing locators obsolete, causing test failures.
- Maintenance overhead: Updating test scripts to accommodate new UI changes is a time consuming and error-prone process.
Introducing Healenium: A Self-Healing Solution
Healenium is an innovative open-source library that helps to addresses these challenge in Appium. It acts as a mediator between your test script and the Appium server, intelligently handling NoSuchElement test failures and adapting to dynamic UI changes.
Benefit of Healenium
- Improved Stability: It employs a sophisticated mechanism to identify multiple potential locators for a UI element, increasing the chances of finding a valid match even if the primary locator fails.
- Dynamic Element Handling: Healenium can dynamically adjust test interactions based on real-time UI changes, ensuring that test scripts remain resilient to modifications.
- Minimize Maintenance: When a test encounters an error, Healenium can attempt to recover by performing alternative locators. This reduces the flakiness in test and result in less maintenance works.
- Root Cause Analysis: Healenium provides detailed reports on test failures, including information about the attempted healing, screenshot and new locators that can help in root cause analysis and test improvement.
- Plugin for Intellij IDE: Healenium provides plugin for Intellij IDE that can automatically update the broken locators in your codebase.
How Healenium Works
Healenium employs a combination of advanced algorithms and techniques to achieve its self-healing capabilities.
It offers two primary integration methods:
- Healenium-Proxy: A versatile, language-agnostic solution positioned between your test runner and the web application. It supports multiple languages and requires configuring your test to connect to the proxy as a RemoteWebDriver.
- Healenium-Web:Specifically for Java, this method integrates directly into your test code for a tighter coupling.
Both approaches leverage Healenium’s core capabilities: intelligent locator selection, dynamic element handling, self-healing actions, and detailed reporting.
As shown in above flow chart, Healenium comprises three core services:
- Healenium Backend: This central component processes healed locators, manages storage, handles screenshots and reports, and orchestrates communication between the other services.
- Healenium DB: A PostgreSQL database storing baseline locators, elements, and healed locators for subsequent runs.
- Healenium Selector Imitator: The core healing engine that reconstructs user-defined selectors based on HTML changes, suggesting potential alternatives while preserving the original selector structure.
Integrating Healenium with Appium
Pre-Requisites
- Docker: Installed and running on your system. As Healenium uses docker to run their self healing mechanism. To install Docker, you can follow the guide: here
- Appium: Set up for mobile test automation as Healenium integrates with Appium. You can refer here for instruction.
- Required language dependencies: Install necessary tools for your test automation framework (e.g., Java, Python, JavaScript).
Step By Step Guide
Step 1: Starting Healenium Proxy:
We can start Healenium proxy with docker-compose file that can start all the 3 services mentioned above and healenium proxy as well in the docker. But We recommend cloning the Healenium repository as it simplifies setup.
- Use the following git command in your terminal to clone the repository.
git clone https://github.com/healenium/healenium.git
- Once cloned, navigate to the root directory of the Healenium repository:
cd ./healenium
- These instructions assume you’re using a local Appium server. If not, update the
file with the following line:docker-compose-appium.yaml
- APPIUM_SERVER_URL=http://host.docker.internal:4723/wd/hub
- Ensure your Docker desktop is running. Then, start Healenium Proxy using the following command:
docker-compose -f docker-compose-appium.yaml up -d
- You’ll see following console logs indicating all four services have started.
- Allow a few minutes for everything to be fully up and running.
- To confirm Healenium Backend is operational, navigate to http://localhost:7878/ in your web browser. You should see the Healenium landing page.
Step 2: Configuring Your Appium Driver
To route Appium driver requests through the Healenium proxy, replace the Appium server URL with the Healenium proxy’s address. This typically looks like following:
// Use Healenium proxy URL instead of Appium server URL
WebDriver driver = new AndroidDriver(new URL("http://localhost:8085"), caps);
Step 3: Execution:
With Healenium Proxy running and the Appium driver configured, you’re ready to execute your first test.
You can use AppiumJava_BoilerPlate for POC.
Note: To enable Healenium’s self-healing, run a successful test initially to establish a baseline of locators.
After completing your test suite, visit http://localhost:7878/healenium/selectors/
to view the saved locators. These serve as a foundation for future healing attempts.
To test Healenium’s self-healing capabilities, modify a locator in your app. For instance, change the “Log In” link to “Sign In” in your test app. Re-run the test.
Note: I’m using my-demo-app-android from SauceLabs for this demo.
Normally, this change would cause the test to fail. However, Healenium should identify the modified element and attempt to heal the locator.
To review healed elements, navigate to http://localhost:7878/healenium/report
and select your test run. This provides details about healed locators and accompanying screenshots.
Conclusion
Healenium is a valuable tool that significantly reduces maintenance efforts for Appium test scripts, especially when dealing with minor UI changes like button text or element IDs. However, it’s essential to be aware of potential drawbacks.
For instance, Healenium’s self-healing capabilities might inadvertently mask UI changes that impact user experience. Let’s say If text of a “Cancel” button is replaced with the text of “Submit” button, Healenium could mistakenly heal the locator, potentially leading to unexpected application behavior.
To mitigate this risk, Healenium offers granular control over which elements should be self-healed. This feature ensures that critical UI changes are not overlooked while benefiting from the tool’s automation capabilities.
Shilpa Patil
Hi,
Thanks for the valuable information. I am stuck at find element screen. Able to run test and redirect to provided browser, but then it fails on finding element.
Yogendra Porwal
Hi Shilpa
Can you please share debug logs from healenium and your tests.
Shilpa Patil
Thank you in advance.
Find logs from appium:
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Driver proxy active, passing request on via HTTP proxy
[575bcee2][Chromedriver@dc30 (be119082)] Matched ‘/session/575bcee2-1d97-41a0-9b12-5e40ca70ba9d/url’ to command name ‘getUrl’
[575bcee2][Chromedriver@dc30 (be119082)] Proxying [GET /session/575bcee2-1d97-41a0-9b12-5e40ca70ba9d/url] to [GET http://127.0.0.1:62512/session/be119082a48f58969dc39094b7de6122/url%5D with no body
[575bcee2][Chromedriver@dc30 (be119082)] Got response with status 200: {“value”:”https://www.amazon.in/”}
[575bcee2][HTTP] POST /wd/hub/session/575bcee2-1d97-41a0-9b12-5e40ca70ba9d/element {“using”:”css selector”,”value”:”input#nav-search-keywords.nav-input nav-progressive-attribute”}
[575bcee2][HTTP] No route found for /wd/hub/session/575bcee2-1d97-41a0-9b12-5e40ca70ba9d/element
[575bcee2][HTTP] DELETE /session/575bcee2-1d97-41a0-9b12-5e40ca70ba9d {}
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Calling AppiumDriver.deleteSession() with args: [“575bcee2-1d97-41a0-9b12-5e40ca70ba9d”]
[575bcee2][AppiumDriver@a7c5] Event ‘quitSessionRequested’ logged at 1733907076901 (14:21:16 GMT+0530 (India Standard Time))
[575bcee2][AppiumDriver@a7c5] Removing session 575bcee2-1d97-41a0-9b12-5e40ca70ba9d from our master session list
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Deleting UiAutomator2 session
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Stopping chromedriver for context CHROMIUM
[575bcee2][Chromedriver@dc30 (be119082)] Changed state to ‘stopping’
[575bcee2][Chromedriver@dc30 (be119082)] Proxying [DELETE /] to [DELETE http://127.0.0.1:62512/session/be119082a48f58969dc39094b7de6122%5D with no body
[575bcee2][Chromedriver@dc30 (be119082)] Got response with status 200: {“value”:null}
[575bcee2][Chromedriver@dc30] Changed state to ‘stopped’
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Deleting UiAutomator2 server session
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Matched ‘/’ to command name ‘deleteSession’
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Proxying [DELETE /] to [DELETE http://127.0.0.1:8200/session/80d5d3bb-4b07-4863-afad-0bce87eff99d%5D with no body
[575bcee2][AndroidUiautomator2Driver@6959 (575bcee2)] Got response with status 200: {“sessionId”:”80d5d3bb-4b07-4863-afad-0bce87eff99d”,”value”:null}
[575bcee2][ADB] Running ‘C:UsersShilpapatilAppDataLocalAndroidSdkplatform-toolsadb.exe -P 5037 -s RZ8N906JCBB shell dumpsys activity services io.appium.settings/.recorder.RecorderService’
[575bcee2][Logcat] Stopping logcat capture
———————————————————————————————————————————————————————————————-Test Logs from IDE:
https://www.amazon.in/
org.openqa.selenium.NoSuchElementException: Failed to find element using By.cssSelector: input#nav-search-keywords.nav-input nav-progressive-attribute
For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
Build info: version: ‘4.11.0’, revision: ‘040bc5406b’
System info: os.name: ‘Windows 11’, os.arch: ‘amd64’, os.version: ‘10.0’, java.version: ‘11.0.19’
Driver info: io.appium.java_client.AppiumDriver
Command: [575bcee2-1d97-41a0-9b12-5e40ca70ba9d, findElement {using=css selector, value=input#nav-search-keywords.nav-input nav-progressive-attribute}]
Capabilities {appium:automationName: uiautomator2, appium:databaseEnabled: false, appium:desired: {appActivity: com.android.chrome/com.goog…, appPackage: com.android.chrome, automationName: uiautomator2, browserName: chrome, deviceName: emulatoe-5554, nativeWebScreenshot: true, platformName: ANDROID}, appium:deviceApiLevel: 31, appium:deviceManufacturer: samsung, appium:deviceModel: SM-M317F, appium:deviceName: RZ8N906JCBB, appium:deviceScreenDensity: 420, appium:deviceScreenSize: 1080×2400, appium:deviceUDID: RZ8N906JCBB, appium:javascriptEnabled: true, appium:locationContextEnabled: false, appium:nativeWebScreenshot: true, appium:networkConnectionEnabled: true, appium:pixelRatio: 2.625, appium:platformVersion: 12, appium:statBarHeight: 87, appium:takesScreenshot: true, appium:viewportRect: {height: 2313, left: 0, top: 87, width: 1080}, appium:warnings: {}, appium:webStorageEnabled: false, browserName: chrome, platformName: ANDROID}
Session ID: 575bcee2-1d97-41a0-9b12-5e40ca70ba9d
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
Shilpa Patil
Error coming in docker- hlm-proxy:
“using”: “accessibility id”,n “value”: “open menu”n }
2024-12-13 14:35:52 2024-12-13 12:05:52.606 WARN 1 – [ttp-epoll-4] healenium : Error during findElement: org.openqa.selenium.UnsupportedCommandException: The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resourcen Build info: version: ‘4.25.0’, revision: ‘8a8aea2337’n System info: os.name: ‘Linux’, os.arch: ‘amd64’, os.version: ‘5.15.167.4-microsoft-standard-WSL2’, java.version: ‘22.0.2’n Driver info: io.appium.java_client.AppiumDrivern Command: [8c7893ac-36dd-463e-a778-00bc1b41ed9b, findElement {using=accessibility id, value=open menu}]n Capabilities {appium:appActivity: com.saucelabs.mydemoapp.rn…., appium:appPackage: com.saucelabs.mydemoapp.rn, appium:automationName: uiautomator2, appium:databaseEnabled: false, appium:desired: {appActivity: com.saucelabs.mydemoapp.rn…., appPackage: com.saucelabs.mydemoapp.rn, automationName: uiautomator2, deviceName: RZ8N906JCBB, newCommandTimeout: 20, noReset: false, platformName: ANDROID, platformVersion: 12}, appium:deviceApiLevel: 31, appium:deviceManufacturer: samsung, appium:deviceModel: SM-M317F, appium:deviceName: RZ8N906JCBB, appium:deviceScreenDensity: 420, appium:deviceScreenSize: 1080×2400, appium:deviceUDID: RZ8N906JCBB, appium:javascriptEnabled: true, appium:locationContextEnabled: false, appium:networkConnectionEnabled: true, appium:newCommandTimeout: 20, appium:noReset: false, appium:pixelRatio: 2.625, appium:platformVersion: 12, appium:statBarHeight: 87, appium:takesScreenshot: true, appium:viewportRect: {height: 2313, left: 0, top: 87, width: 1080}, appium:warnings: {}, appium:webStorageEnabled: false, platformName: ANDROID}n Session ID: 8c7893ac-36dd-463e-a778-00bc1b41ed9bn at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)n at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)n at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)n at org.openqa.selenium.remote.ErrorCodec.decode(ErrorCodec.java:167)n at org.openqa.selenium.remote.codec.w3c.W3CHttpResponseCodec.decode(W3CHttpResponseCodec.java:138)n at org.openqa.selenium.remote.codec.w3c.W3CHttpResponseCodec.decode(W3CHttpResponseCodec.java:50)n at org.openqa.selenium.remote.HttpCommandExecutor.execute(HttpCommandExecutor.java:190)n at com.epam.healenium.healenium_proxy.command.HealeniumCommandExecutor.execute(HealeniumCommandExecutor.java:31)n at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:545)n at org.openqa.selenium.remote.ElementLocation$ElementFinder$2.findElement(ElementLocation.java:165)n at org.openqa.selenium.remote.ElementLocation.findElement(ElementLocation.java:66)n at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:368)n at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:362)n at com.epam.healenium.processor.FindElementProcessor.execute(FindElementProcessor.java:23)n at com.epam.healenium.processor.BaseProcessor.process(BaseProcessor.java:42)n at com.epam.healenium.handlers.proxy.BaseHandler.findElement(BaseHandler.java:63)n at com.epam.healenium.healenium_proxy.filter.FindElementRequestGatewayFilterFactory.findElement(FindElementRequestGatewayFilterFactory.java:62)n at com.epam.healenium.healenium_proxy.filter.FindElementRequestGatewayFilterFactory.lambda$apply$0(FindElementRequestGatewayFilterFactory.java:48)n at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:132)n at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)n at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299)n at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)n at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097)n at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145)n at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)n at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260)n at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)n at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:415)n at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:445)n at reactor.netty.http.server.HttpServerOperations.handleLastHttpContent(HttpServerOperations.java:862)n at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:784)n at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:115)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)n at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)n at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:311)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)n at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)n at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)n at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)n at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)n at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)n at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)n at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1407)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)n at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)n at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)n at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799)n at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:501)n at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:399)n at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994)n at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)n at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)n at java.base/java.lang.Thread.run(Thread.java:1570)n